feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool - #2122
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
Requesting changes on Wes's behalf after full review in Buzz. The architecture is sound — the lazy-pool state machine, pair receipts, and replace-before-spawn seam are well-built and well-tested — but four concrete issues need to land first:
- CI is red. Desktop Core fails
desktop-tauri-clippywith 3 errors:ManagedAgentRuntimeTargetis never constructed (dead type inruntime_types.rs— no command takes it),write_agent_pid_fileis never used (its last caller was removed; keep it for migration or delete it), and amanual_ignore_case_cmplint inobserver_lifecycle_key(useeq_ignore_ascii_case). - Members-sidebar badge regression.
MembersSidebargained arelayUrl?prop, butChannelScreen— its only call site — never passes it, somanagedAgentRuntimeis alwaysundefinedand every managed agent now renders a visible "Unavailable" badge regardless of actual state (previously an sr-only status). Wire the prop through, or don't render the fallback badge when runtime state is unknown. - Lane D battery isn't in the diff. Only the fixtures shipped (
fake-acp-agent.mjs,twoRelayHarness.ts); nothing importsTwoRelayHarness, so the "integration matrix incl. native replacement probe" described in the PR body isn't in this PR. Either include the specs or state explicitly that they're a follow-up — as merged, this is dead test code. reconcile_managed_agent_runtimesignoresstart_on_app_launch. The launch-restore path filters on it (restore.rs:158), but the new startup reconcile — called unconditionally fromAppShell— spawns every local agent in every membership community regardless of that setting. If "every agent, everywhere" deliberately supersedes the setting, say so and reconcile the two paths; right now an agent the user opted out of auto-start gets started anyway.
What's good, for the record: the PoolLifecycle attempt-token design (stale wakes can't clobber a newer pool; abandoned pools are shut down on both the send-fail path and the post-loop drain), fail-closed receipt replacement (receipt removed only after confirmed exit, tested via an injectable seam), a single canonical relay-URL normalizer explicitly kept separate from the NIP-42 AUTH comparison, and documented lock discipline on managed_agent_runtime_transition. Re-review will be quick once the above land.
Reviewed-by: Brain (Buzz agent) on behalf of Wes
|
Thanks for the thorough review, Wes. All four items are addressed at the current head
Since your review, the branch also picked up two plain merge commits of CI was fully green at |
ed55dcd to
f435d08
Compare
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
restore_managed_agents_on_launch spawned every auto-start pair with lazy=false, booting an eager LLM child on every ordinary launch and silently reintroducing N idle brains — the exact idle cost the pair runtime is meant to avoid. It also decided "untracked" from receipts alone, so a startup reconcile that spawned the same pair during the Phase A window (before restore takes the transition lock) could be killed and replaced eager, flipping the pair's laziness on a race. F1: spawn restore children lazy=true, matching startup reconcile and manual start. Eager on restore buys nothing — a crashed mid-turn session is not resumed by an eager child; the next mention wakes a lazy one just as well. F2: before terminate-and-respawn, check the live process map for an already-tracked live child at this exact pair key and skip if present, mirroring start_pair's live-child guard. Leaves a concurrently reconciled child untouched instead of killing and replacing it. Live restore/reconcile-race integration coverage (real process table + AppHandle) is deferred to the Lane D battery; the inlined guard is not unit-testable in isolation without an AppHandle. Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Pair-scoped stop_managed_agent_runtime removed the runtime from the map before fallible terminate/wait and bailed with ?, leaving a live child untracked (and its receipt gone) on failure — the same invisible-orphan class the agent-wide stop path already handles. On terminate/wait failure, reinsert the runtime and return the error so the pair stays visible and stoppable. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Complete the pair-runtime fix plan so each community's UI tells the truth about its own (agent, community) pair: - Summary scoping: build_managed_agent_summary resolves the viewed workspace's pair key (relay pin, else active workspace relay) and looks up status/pid/log and needs_restart by exact pair key — an agent running only in another community now reads as stopped here, and restart drift is hashed against the pair's own relay instead of the currently viewed workspace's, so community switches no longer flag spurious restarts. - Pair-scoped stop: the user-facing stop_managed_agent command stops only the active workspace's pair via the new stop_managed_agent_workspace_pair (extracted per-pair teardown with reinsert-on-failure), clearing only that pair's session cache; delete and config-restart flows keep draining every pair through stop_managed_agent_process. The auto-restart policy now bounces only the drifted pair for the viewed community (safety comment updated). - Reconcile bootstrap: rename discover_agent_membership to probe_agent_relay_access and gate spawn eligibility on the bounded authenticated probe succeeding rather than kind-39002 membership presence, which cannot exist before a harness first connects and so could never bootstrap a new pair; the reconcile also skips communities whose relay conflicts with an explicit per-record pin. - Post-create bootstrap: creating a local agent fires an idempotent reconcile across all configured communities so the new agent gets a lazy pair everywhere immediately instead of at the next launch; the E2E bridge answers reconcile_managed_agent_runtimes with an empty set so Playwright runs don't throw. Tests: pure pair-key resolution (resolve_workspace_pair_key) covering unpinned per-workspace keys, pin precedence, canonicalization, and invalid pubkeys; exact-pair stop selection; and reconcile pin filtering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Pair-scoped stop_managed_agent_runtime removed the runtime receipt without any termination attempt when no runtime was tracked at the pair key. If a valid prior-session receipt still pointed at a live child (the crash-recovery window for a non-auto-start agent), this stop left the harness running yet erased the one artifact that sweeps and terminate_untracked_pair_runtime use to find the orphan, so a follow-up start would then spawn a duplicate harness for the same pair. That was asymmetric with the tracked path, which keeps the receipt on disk until a stop actually succeeds. Route the untracked case through terminate_untracked_pair_runtime before removing the receipt: it terminates a live child from a valid receipt and deletes the receipt only after the child exits, so on failure the receipt stays on disk — restoring the tracked path's keep-until-success invariant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
The per-record relay_url pin predates workspace-scoped runtimes. Honoring it made pinned agents silently skip fan-out to other communities (the bug Tyler hit live). Instead of migrating/clearing stored pins, ignore them: - effective_agent_relay_url() now always returns the workspace relay; the stored pin is parsed and persisted untouched, so old records read identically and rollback to a pin-honoring build is safe. - reconcile_managed_agent_runtimes drops the record_allows_relay filter: every start_on_app_launch agent fans out to every community. - Spawn-hash/pair-key docs and tests updated: workspace relay changes trip the hash even for pinned records; editing the ignored pin does not. - Relay URL editor field removed from the agent edit dialog; relayUrl is never submitted so the stored value is preserved. Part of #2122. Co-authored-by: Tyler Longwell <tlongwell@block.xyz> Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
…ad-lifecycle * origin/main: fix(desktop): strip GIF metadata extensions before upload (#2425) feat(desktop): gate sign-out behind key backup + typed confirmation (#2424) ci(desktop): add signed macOS canary build (#2419) feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122) Show team count separately in channel template rows (#2404) fix(dev): restore shared worktree identity from keyring (#2400) fix(onboarding): skip community profile setup for existing relay members (#2300) fix(mobile): preserve and display sidebar section icons (#2403) Unify sidebar chrome across themes (#2380) feat(media): add S3-truth per-community storage sweep (#2044)
…obile-releasing Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> * origin/main: fix(desktop): strip GIF metadata extensions before upload (#2425) feat(desktop): gate sign-out behind key backup + typed confirmation (#2424) ci(desktop): add signed macOS canary build (#2419) feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122) Show team count separately in channel template rows (#2404) fix(dev): restore shared worktree identity from keyring (#2400) fix(onboarding): skip community profile setup for existing relay members (#2300) fix(mobile): preserve and display sidebar section icons (#2403) Unify sidebar chrome across themes (#2380) feat(media): add S3-truth per-community storage sweep (#2044) feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests (#2206) Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co> # Conflicts: # RELEASING.md
…ad-lifecycle * origin/main: fix(desktop): strip GIF metadata extensions before upload (#2425) feat(desktop): gate sign-out behind key backup + typed confirmation (#2424) ci(desktop): add signed macOS canary build (#2419) feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122) Show team count separately in channel template rows (#2404) fix(dev): restore shared worktree identity from keyring (#2400) fix(onboarding): skip community profile setup for existing relay members (#2300) fix(mobile): preserve and display sidebar section icons (#2403) Unify sidebar chrome across themes (#2380) feat(media): add S3-truth per-community storage sweep (#2044) Signed-off-by: npub1dpf98sl35hm9k65t8h6cvh5n6knn2msugh5k5nmysxwnw5wlh7uqn2wgp4 <685253c3f1a5f65b6a8b3df5865e93d5a7356e1c45e96a4f64819d3751dfbfb8@sprout-oss.stage.blox.sqprod.co>
…e canonical runtime key Since block#2122, spawn paths passed ManagedAgentRuntimeKey.relay_url (the canonical identity form) to spawned children as BUZZ_RELAY_URL. The canonicalizer folds every loopback spelling to 127.0.0.1, but the relay derives the community boundary from the Host header, so a community configured as ws://localhost:3000 had its UI post into the localhost:3000 community while every agent connected to the 127.0.0.1:3000 community. Agents discovered zero channels, logged 'no channel subscriptions resolved - agent will sit idle', and silently never answered mentions. Per the contract documented on buzz_core::relay::normalize_relay_url ('Connection code may retain the configured URL; this canonical form is for identity'), keep the canonical key for identity, receipts, and log paths, and dial the caller-supplied requested URL: - spawn_agent_child dials the relay_url parameter verbatim; the runtime key remains identity-only - start_managed_agent_process, start_pair, and launch restore pass their already-resolved requested URL instead of key.relay_url - reconcile_managed_agent_runtimes forwards the requested community URL to start_pair instead of the canonical form - probe_agent_relay_access probes over the requested URL so the HTTP probe hits the same community the child will join - start_pair now reports requested_relay_url in its status rows, and the Active Communities settings card prefers requestedRelayUrl for start/stop/restart actions so a manual restart re-dials the configured URL rather than the canonical echo No new automated test covers the spawn wiring itself (it forks a real child process); verified with cargo test (1560 passed), clippy, fmt, tsc, and biome. Fixes block#2444 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Lewis Wang <lewis.wang@plusalsostudios.com.au>
Rebases the gate-cleared ledger branch (715779e) onto origin/main (4253688), which gained two commits touching buzz-acp/src/lib.rs: - #2122: per-(agent, community) harness at GUI startup — lazy pool, pool_lifecycle.rs, pool_ready flag, PoolEvent::Wake arm. - #2217: relay observer frame pacing (ObserverPublishPacer, subscribe- before-snapshot race fix). Conflict resolution (lib.rs only; config.rs/pool.rs/relay.rs/queue.rs auto-merged cleanly): 1. use std::collections: merged VecDeque (#2122) + PathBuf (branch). 2. use queue: merged pool_lifecycle::PoolLifecycle (#2122) + BatchDisposition (branch). 3. invalidate_channel_sessions: kept #2122's pool_ready guard AND added branch's ledger.invalidate_channel call. 4. relay-event dispatch_pending: kept #2122's pool_ready guard AND added branch's &mut ledger argument. 5. heartbeat dispatch_pending: combined #2122's pool_not_ready early-skip with branch's let-flushable/sync_dirty logic (extra nesting level). Also fixed two test fixtures missing new fields (lazy_pool in test_config, relay_url in test_prompt_context_with_dedup_mode) — both from #2122 additions that auto-merged into the non-test code. Post-merge also threaded &mut ledger into the PoolEvent::Wake → dispatch_pending call site (came from main's auto-merged #2122 code; had the pre-branch signature without ledger). Gates: cargo test -p buzz-acp (674 passed, 0 failed), cargo clippy -D warnings (clean), just fmt-check (clean), just clippy (clean). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…elay clobber #2122 spawns one harness per (agent, relay) pair sharing a workdir. The ledger file was keyed by agent pubkey only; two harnesses for the same agent on different relays would load the same file and each boot would commit a snapshot membership-gated to its own channels, silently erasing the other relay's pending turns. Namespace the filename as pending-turns-<pubkey16>-<relayhash16>.json, where relayhash16 is the first 16 hex chars of the sha256 of the canonical relay URL (same normalization Desktop uses for runtime_id). Each (agent, relay) pair now owns a distinct file; concurrent harnesses never share state. Also expose a #[cfg(test)] Ledger::path() accessor so the atomic-write tests in lib.rs can derive the correct .tmp sibling path without re-implementing the filename logic. Migration: pre-merge builds wrote pending-turns-<pubkey16>.json. The feature has never shipped (PR is DRAFT), so old-format files are simply ignored on first load — one lost recovery opportunity at worst, the same floor as a clean install. No migration code needed. Duplicate-writer risk: #2122 spawns at most one process per (agent, relay) key (ManagedAgentRuntimeKey is a HashMap key and processes are keyed by it). A same- pair duplicate writer is not possible under the current lifecycle; no lock needed. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Harnesses became per (agent, relay) pair in #2122 and now write `agents/logs/{pubkey}__{sha256(relay_url)}.log` via `managed_agent_runtime_log_path`. `get_managed_agent_log` was never updated and still read the legacy `agents/logs/{pubkey}.log`, so agent profile → Runtime → Harness Log froze at each agent's last single-runtime line while live output accumulated in files the reader never opened. The reader now resolves the log through `latest_managed_agent_log_path`, which picks the most recently modified file belonging to the agent — pair-scoped `{pubkey}__*.log` or legacy `{pubkey}.log` — and falls back to the legacy path when the agent has no log on disk at all. Agents that have not restarted since the update keep working, and the panel follows whichever harness is currently writing. The response already carried `log_path`, so the panel header names the file being shown. Selection is deterministic: equal mtimes break toward the higher filename, and files belonging to other agents or without a `.log` extension are never candidates. `storage.rs`'s inline test module moves to a `#[path]`-included sibling `storage_tests.rs`, matching the existing pattern in `teams.rs` and `archive/mod.rs`. This drops both halves under the desktop file-size limit (1383 → 826 / 701), so the ratchet entries tighten instead of growing. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Harnesses became per (agent, relay) pair in block#2122 and now write `agents/logs/{pubkey}__{sha256(relay_url)}.log` via `managed_agent_runtime_log_path`. `get_managed_agent_log` was never updated and still read the legacy `agents/logs/{pubkey}.log`, so agent profile → Runtime → Harness Log froze at each agent's last single-runtime line while live output accumulated in files the reader never opened. The reader now resolves the log through `latest_managed_agent_log_path`, which picks the most recently modified file belonging to the agent — pair-scoped `{pubkey}__*.log` or legacy `{pubkey}.log` — and falls back to the legacy path when the agent has no log on disk at all. Agents that have not restarted since the update keep working, and the panel follows whichever harness is currently writing. The response already carried `log_path`, so the panel header names the file being shown. Selection is deterministic: equal mtimes break toward the higher filename, and files belonging to other agents or without a `.log` extension are never candidates. `storage.rs`'s inline test module moves to a `#[path]`-included sibling `storage_tests.rs`, matching the existing pattern in `teams.rs` and `archive/mod.rs`. This drops both halves under the desktop file-size limit (1383 → 826 / 701), so the ratchet entries tighten instead of growing. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
## Buzz Relay release v0.2.1 ### Changes since relay-v0.2.0: - fix(sdk): preserve self-mention p tags in message and forum event builders ([#4975](#4975)) ([`78c87ae20e`](78c87ae)) - feat(desktop): adding rich link previews to messages ([#3818](#3818)) ([`1922d49cb2`](1922d49)) - feat(relay): accept kind:30179 private managed-agent events at ingest ([#5133](#5133)) ([`ad923353a2`](ad92335)) - fix(media): require authenticated reads ([#4610](#4610)) ([`769ac70b74`](769ac70)) - feat(identity): recover desktop identity from a signed-in phone ([#4845](#4845)) ([`6eb65919f1`](6eb6591)) - ci: prove the relay-driven mesh lifecycle — discover, join, infer, deny — with real nodes ([#3862](#3862)) ([`38bf642fcf`](38bf642)) - relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) ([#4542](#4542)) ([`e14fff74d0`](e14fff7)) - fix(reactions): support max-length custom emoji ([#3833](#3833)) ([`2ea9385015`](2ea9385)) - fix(channels): restrict private-channel invitations ([#4612](#4612)) ([`efe1893dd3`](efe1893)) - fix(workflow): bind trigger author to the signed event ([#4607](#4607)) ([`885bed35ee`](885bed3)) - fix(git): revoke access for banned relay members ([#4608](#4608)) ([`997b8caaa4`](997b8ca)) - Define private managed agent wire protocol ([#4593](#4593)) ([`067c085f37`](067c085)) - perf(relay): index channel-id lookups and skip trace-only reads ([#4647](#4647)) ([`bc9e6528a7`](bc9e652)) - Polish mobile inbox and media flows ([#4512](#4512)) ([`feccf4eabc`](feccf4e)) - fix(git): allow deleting the default branch ([#4297](#4297)) ([`fc598f5f8d`](fc598f5)) - feat(projects): add buzz projects CLI commands (NIP-MP kind:30621) ([#4020](#4020)) ([`b7bb15122e`](b7bb151)) - perf(relay): serve relay-membership checks from the read replica ([#4124](#4124)) ([`ac4fa13b8e`](ac4fa13)) - fix(relay): allow open relays to set their NIP-11 workspace icon (kind:9033) ([#3998](#3998)) ([`5765fc74b7`](5765fc7)) - feat(relay): accept kind:30621 multi-repo projects at ingest ([#3171](#3171)) ([`cb9701cd30`](cb9701c)) - feat(relay): raise hosted community limit to five ([#3829](#3829)) ([`10d5a26414`](10d5a26)) - fix(relay): align NIP-11 max_limit with REQ ceiling ([#3635](#3635)) ([`23f0c26b1c`](23f0c26)) - feat(relay): gate kind 30178 team-catalog reads behind the shared tag ([#3358](#3358)) ([`114d40d9d3`](114d40d)) - fix(db): isolate usage metrics advisory-lock test on scratch DB ([#3670](#3670)) ([`dba97eecd9`](dba97ee)) - perf(presence): reduce heartbeat frequency ([#3783](#3783)) ([`bf139e8d0b`](bf139e8)) - feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) ([#3741](#3741)) ([`4933672eb4`](4933672)) - feat(replica): portable heartbeat-token fence with snapshot-local reader routing ([#3268](#3268)) ([`63496cc1d4`](63496cc)) - fix(git): channel binding tooling + author remediation for unbound repos ([#3626](#3626)) ([`788b3c002b`](788b3c0)) - feat: configure S3 URL addressing style ([#3400](#3400)) ([`7012d86d52`](7012d86)) - feat(tracing): correlate trace IDs in relay logs ([#3608](#3608)) ([`005b5b819a`](005b5b8)) - fix(relay): avoid subscription lock inversion ([#3413](#3413)) ([`22be8bb351`](22be8bb)) - feat(cli): add users set-status command for NIP-38 profile status ([#3253](#3253)) ([`60158fce3e`](60158fc)) - feat(relay): make Postgres pool size configurable, default 50 ([#3191](#3191)) ([`2ce2d71cc3`](2ce2d71)) - feat(tracing): add datastore tracing plumbing ([#2760](#2760)) ([`e94b9aeda0`](e94b9ae)) - feat(invites): add use-limited invite links ([#3141](#3141)) ([`d500c2d5cf`](d500c2d)) - feat(admin): show reported message content in report detail ([#3149](#3149)) ([`f069a85503`](f069a85)) - resolve findings ([#3150](#3150)) ([`9b0f744804`](9b0f744)) - Revert "fix(cli,relay): resolve agents by verified owner" ([#3168](#3168)) ([`a041e2d21e`](a041e2d)) - fix(cli,relay): resolve agents by verified owner ([#2615](#2615)) ([`c3084b36d9`](c3084b3)) - fix(security): enforce durable community ban on NIP-43 relay-admin kinds 9030-9033 ([#3128](#3128)) ([`e2e0079101`](e2e0079)) - fix(security): authorize kind:9000 role changes in both directions ([#3017](#3017)) ([`00ecf2cac7`](00ecf2c)) - feat(desktop): handle project work from Inbox ([#3117](#3117)) ([`c5c4f390b6`](c5c4f39)) - feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER ([#2599](#2599)) ([`2a051a404d`](2a051a4)) - feat(relay): add author-only-unless-shared read gate for kind 30175 ([#2768](#2768)) ([`ab3af82871`](ab3af82)) - fix(core): block IPv6 transition SSRF targets ([#2801](#2801)) ([`c26bf5945d`](c26bf59)) - fix(workflow): bypass system proxies for webhooks ([#2800](#2800)) ([`60a171b19e`](60a171b)) - fix(audit): hash created_at at the precision Postgres stores ([#2638](#2638)) ([`264a56a226`](264a56a)) - feat(desktop): make pull request reviews actionable ([#2510](#2510)) ([`9081ab0ec9`](9081ab0)) - fix(relay): decompress gzip-encoded git smart-HTTP request bodies ([#2670](#2670)) ([`5ca36e7b91`](5ca36e7)) - fix(sharing): preserve agent/team snapshot tEXt chunks through media sanitization ([#2438](#2438)) ([`b096b0a15a`](b096b0a)) - fix(relay): send 1012 restart close to all clients on graceful drain ([#2575](#2575)) ([`1911c69aa2`](1911c69)) - fix(media): sanitize animated image uploads ([#2524](#2524)) ([`8f8f5fa5a4`](8f8f5fa)) - fix(channels): strip leading hash prefixes from names ([#2250](#2250)) ([`d0ab3fdb05`](d0ab3fd)) - feat(relay): make Redis pool size configurable, default 16 ([#2521](#2521)) ([`bcc3e13069`](bcc3e13)) - feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool ([#2122](#2122)) ([`61cc738ee8`](61cc738)) - feat(media): add S3-truth per-community storage sweep ([#2044](#2044)) ([`bd37a4d584`](bd37a4d)) - feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests ([#2206](#2206)) ([`7e34bee62c`](7e34bee)) - Revert "feat(relay): inventory unreachable Git objects" ([#2275](#2275)) ([`0fb820f9bf`](0fb820f)) - feat(relay): inventory unreachable Git objects ([#2264](#2264)) ([`3afc9dae15`](3afc9da)) - relay: add author_type label to buzz_events_stored_total ([#2243](#2243)) ([`b9f54c43fe`](b9f54c4)) - fix(git): make project branch workflows reliable ([#2213](#2213)) ([`166f27be4b`](166f27b)) - feat(cli): manage repository protection rules ([#2193](#2193)) ([`f94324598d`](f943245)) - feat(cli): add agents archive/unarchive/archived subcommands ([#2173](#2173)) ([`7d7992067b`](7d79920)) - fix(mobile): sanitize Android image uploads ([#2188](#2188)) ([`ee21da90bd`](ee21da9)) - fix(cli): paginate channel directory queries ([#2181](#2181)) ([`03fe19d603`](03fe19d)) - fix(mobile): image upload fails due to unstripped metadata ([#2185](#2185)) ([`37f15b2001`](37f15b2)) - perf(relay): compact Git packs before manifest limits ([#2172](#2172)) ([`80e0ab16b0`](80e0ab1)) - perf(relay): cache Git pack hydration ([#2169](#2169)) ([`a4d82ec722`](a4d82ec)) - fix(relay): bound and observe Git read operations ([#2167](#2167)) ([`5f7c93d9c1`](5f7c93d)) - relay: gate push enqueue on live leases; batch matcher pipeline (T1b/T1a-repair/T2b) ([#2145](#2145)) ([`e43b2d5aac`](e43b2d5)) - relay: add audit logging disable switch ([#2134](#2134)) ([`bf5acabdde`](bf5acab)) - relay: skip TTL deadline bump for known-permanent channels (T1a write-amp) ([#2125](#2125)) ([`2e936d439c`](2e936d4)) - fix(git): carry NIP-OA delegation in auth event ([#2120](#2120)) ([`c12257d57a`](c12257d)) - Route lag-tolerant reads to an optional Postgres read replica ([#2084](#2084)) ([`29c48883d3`](29c4888)) - fix: recover community access visibility ([#2074](#2074)) ([`ca384d082d`](ca384d0)) - feat: proxy feedback-scoped admin attachments ([#2059](#2059)) ([`d7f918e3cb`](d7f918e)) - feat: add read-only deployment moderation dashboard ([#1999](#1999)) ([`68e670e001`](68e670e)) - Bug-bash round 2: table scroll, Goose instructions, workflow mention wake ([#2034](#2034)) ([`64b8fea6dc`](64b8fea)) - Strip media metadata on clients and reject it at the relay ([#2006](#2006)) ([`5cfd69cb0c`](5cfd69c)) - [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018) ([#1916](#1916)) ([`7baea42abb`](7baea42)) - [codex] Enforce shared relay admission limits (BUZZ-SEC-019) ([#1917](#1917)) ([`73fc0ec6cf`](73fc0ec)) - [codex] Block banned actors from moderation commands (BUZZ-SEC-007) ([#1915](#1915)) ([`caa195ca58`](caa195c)) - [codex] Fix relay WebSocket admission limits ([#1682](#1682)) ([`d3ce971fc7`](d3ce971)) - feat: add invite QR and mobile direct join ([#1957](#1957)) ([`648cbf3610`](648cbf3)) - fix(join-policy): require legal consent on hosted invites ([#1987](#1987)) ([`2e1577f76f`](2e1577f)) - [codex] Prevent actor-tag UI impersonation ([#1931](#1931)) ([`c540ec9678`](c540ec9)) - Scope relay runtime state by community ([#1658](#1658)) ([`d52dedb06f`](d52dedb)) - Apply optional relay join policy across join flows ([#1894](#1894)) ([`6c2d667575`](6c2d667)) - feat(media): require auth for relay media reads ([#1926](#1926)) ([`f308762852`](f308762)) - feat(relay): add community unarchive endpoint ([#1908](#1908)) ([`6b9641db2b`](6b9641d)) - feat(relay): gate Git web GUI separately ([#1901](#1901)) ([`34dc7dec75`](34dc7de)) - mesh: upgrade runtime, enforce membership, add shared compute provider ([#1656](#1656)) ([`54638ff4bb`](54638ff)) - Route Git scratch through configured volume ([#1884](#1884)) ([`2318b3096c`](2318b30)) - feat(relay): gate usage metrics behind stable leader ([#1814](#1814)) ([`59e9821503`](59e9821)) - Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh) ([#1670](#1670)) ([`ccb021d713`](ccb021d)) - feat(push): deliver accepted relay events as wakes ([#1866](#1866)) ([`bffbc5f22c`](bffbc5f)) - fix(db): resolve duplicate migration version ([#1863](#1863)) ([`08ad38a07f`](08ad38a)) - Add private product feedback sidecar ([#1857](#1857)) ([`af190c93e1`](af190c9)) - feat(relay): add durable community archival ([#1834](#1834)) ([`2b15a72675`](2b15a72)) - feat(push): add public APNs gateway ([#1770](#1770)) ([`1c006822e4`](1c00682)) - feat(relay): add atomic community ownership transfer ([#1845](#1845)) ([`52e42ccb9f`](52e42cc)) - Bound NIP-RS retention and search indexing ([#1771](#1771)) ([`1b4703021d`](1b47030)) - Add optional standalone pairing relay to Helm chart ([#1799](#1799)) ([`9b47c8548f`](9b47c85)) - fix(relay): publish membership snapshot on provisioning ([#1761](#1761)) ([`0950d392b7`](0950d39)) - feat(relay): per-community usage metrics ([#1723](#1723)) ([`620822899a`](6208228)) - refactor(desktop): remove vestigial MCP toolsets config ([#1776](#1776)) ([`dfec75b3c0`](dfec75b)) **To release:** merge this PR. The tag and build will happen automatically. Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Managed agents live in one global store shared by every community, so every picker offered every record — including several identically-named identities provisioned separately per community (three "Bumble"s, distinguishable only by npub). Only one has a harness in the community being viewed; mentioning either of the others is silently dead, because nothing fails: the p-tag is delivered to a relay where that pubkey was never started. Model (matches Slack/GitHub/Discord: global definition, per-tenant installed identity): a persona stays global; a managed agent instance now belongs to a community. New `community_relay_url` on the record (canonical relay URL; null = unscoped/shared, offered everywhere). Display and name-uniqueness scope ONLY — spawn resolution (`effective_agent_relay_url`, agents-everywhere block#2122) is untouched, so the change is rollback-safe. - Field: `#[serde(default)]`, deliberately NO `skip_serializing_if` — the boot backfill is idempotent by key presence, so an explicit unscope must persist a literal null or the next boot would re-bind it from the legacy pin. - Boot backfill (migration/community_scope.rs): derives the binding from the legacy creation-era `relay_url` pin when present; blank pin carries no evidence and stays unscoped. Verified byte-stable on a second run against a copy of a live store. - Creation stamps the active community at all three minting sites and enforces per-(community, name) uniqueness inside the store lock (case-insensitive, instances only, unscoped collides everywhere; create-time only — pre-existing duplicates keep working). The create dialog preflights the same rule client-side so a collision rejects inline instead of surfacing after the persona exists. - Pickers scope through `managedAgentBelongsToCommunity` (directory presence kept as an override: an agent registered here is mentionable here regardless of its binding): mentions, new-message recipients, members sidebar, projects prompt, Pulse, and the Agents page — which partitions into in-community + a collapsed "From other communities" group. Resolution of already-present pubkeys (history, membership, runtime, search authors, tray) stays deliberately unfiltered. - New `set_managed_agent_community` command + `AgentCommunityScopeBadge` (badge + menu on agent cards/rows): assign to the active community, move, or share across all communities. File-size ratchet extractions along the way: migration/json_patch.rs, managed_agents/{community_scope,record_views}.rs, commands/agent_create_support.rs, shared/api/{managedAgent, managedAgentRaw}.ts, messages/lib/managedAgentMentionMaps.ts, and two exhaustive Rust test fixtures converted to the JSON-fixture convention. Verified: cargo test 2283 passed; desktop tests 4562 passed; tsc, clippy -D warnings, cargo fmt, pnpm check, git diff --check all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com>
Managed agents live in one global store shared by every community, so every picker offered every record — including several identically-named identities provisioned separately per community (three "Bumble"s, distinguishable only by npub). Only one has a harness in the community being viewed; mentioning either of the others is silently dead, because nothing fails: the p-tag is delivered to a relay where that pubkey was never started. Model (matches Slack/GitHub/Discord: global definition, per-tenant installed identity): a persona stays global; a managed agent instance now belongs to a community. New `community_relay_url` on the record (canonical relay URL; null = unscoped/shared, offered everywhere). Display and name-uniqueness scope ONLY — spawn resolution (`effective_agent_relay_url`, agents-everywhere block#2122) is untouched, so the change is rollback-safe. - Field: `#[serde(default)]`, deliberately NO `skip_serializing_if` — the boot backfill is idempotent by key presence, so an explicit unscope must persist a literal null or the next boot would re-bind it from the legacy pin. - Boot backfill (migration/community_scope.rs): derives the binding from the legacy creation-era `relay_url` pin when present; blank pin carries no evidence and stays unscoped. Verified byte-stable on a second run against a copy of a live store. - Creation stamps the active community at all three minting sites and enforces per-(community, name) uniqueness inside the store lock (case-insensitive, instances only, unscoped collides everywhere; create-time only — pre-existing duplicates keep working). The create dialog preflights the same rule client-side so a collision rejects inline instead of surfacing after the persona exists. - Pickers scope through `managedAgentBelongsToCommunity` (directory presence kept as an override: an agent registered here is mentionable here regardless of its binding): mentions, new-message recipients, members sidebar, projects prompt, Pulse, and the Agents page — which partitions into in-community + a collapsed "From other communities" group. Resolution of already-present pubkeys (history, membership, runtime, search authors, tray) stays deliberately unfiltered. - New `set_managed_agent_community` command + `AgentCommunityScopeBadge` (badge + menu on agent cards/rows): assign to the active community, move, or share across all communities. File-size ratchet extractions along the way: migration/json_patch.rs, managed_agents/{community_scope,record_views}.rs, commands/agent_create_support.rs, shared/api/{managedAgent, managedAgentRaw}.ts, messages/lib/managedAgentMentionMaps.ts, and two exhaustive Rust test fixtures converted to the JSON-fixture convention. Verified: cargo test 2283 passed; desktop tests 4562 passed; tsc, clippy -D warnings, cargo fmt, pnpm check, git diff --check all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com>
* feat(desktop): scope managed agents per community Managed agents live in one global store shared by every community, so every picker offered every record — including several identically-named identities provisioned separately per community (three "Bumble"s, distinguishable only by npub). Only one has a harness in the community being viewed; mentioning either of the others is silently dead, because nothing fails: the p-tag is delivered to a relay where that pubkey was never started. Model (matches Slack/GitHub/Discord: global definition, per-tenant installed identity): a persona stays global; a managed agent instance now belongs to a community. New `community_relay_url` on the record (canonical relay URL; null = unscoped/shared, offered everywhere). Display and name-uniqueness scope ONLY — spawn resolution (`effective_agent_relay_url`, agents-everywhere block#2122) is untouched, so the change is rollback-safe. - Field: `#[serde(default)]`, deliberately NO `skip_serializing_if` — the boot backfill is idempotent by key presence, so an explicit unscope must persist a literal null or the next boot would re-bind it from the legacy pin. - Boot backfill (migration/community_scope.rs): derives the binding from the legacy creation-era `relay_url` pin when present; blank pin carries no evidence and stays unscoped. Verified byte-stable on a second run against a copy of a live store. - Creation stamps the active community at all three minting sites and enforces per-(community, name) uniqueness inside the store lock (case-insensitive, instances only, unscoped collides everywhere; create-time only — pre-existing duplicates keep working). The create dialog preflights the same rule client-side so a collision rejects inline instead of surfacing after the persona exists. - Pickers scope through `managedAgentBelongsToCommunity` (directory presence kept as an override: an agent registered here is mentionable here regardless of its binding): mentions, new-message recipients, members sidebar, projects prompt, Pulse, and the Agents page — which partitions into in-community + a collapsed "From other communities" group. Resolution of already-present pubkeys (history, membership, runtime, search authors, tray) stays deliberately unfiltered. - New `set_managed_agent_community` command + `AgentCommunityScopeBadge` (badge + menu on agent cards/rows): assign to the active community, move, or share across all communities. File-size ratchet extractions along the way: migration/json_patch.rs, managed_agents/{community_scope,record_views}.rs, commands/agent_create_support.rs, shared/api/{managedAgent, managedAgentRaw}.ts, messages/lib/managedAgentMentionMaps.ts, and two exhaustive Rust test fixtures converted to the JSON-fixture convention. Verified: cargo test 2283 passed; desktop tests 4562 passed; tsc, clippy -D warnings, cargo fmt, pnpm check, git diff --check all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): scope agent reuse and Welcome selection to the community Addresses review findings 1, 2 and 4. Finding 1 — backfill scope before typed store rewrites. `community_relay_url` is `#[serde(default)]` with no `skip_serializing_if`, and the persona fold deserializes and re-serializes the whole managed-agent store, so it turns "key absent" into an explicit `null`. Running the backfill after the fold therefore stranded every pre-existing instance on any upgrade that still had personas.json: the key-presence idempotency guard read that null as a deliberate unscope and never derived the binding from the non-empty legacy pin. The backfill now runs before the first typed rewrite; the steps ahead of it are raw JSON patches, so key absence still survives to that point, and records added later serialize their own explicit null, which is the correct unscoped binding for a definition. Corrects the fold's doc comment, which claimed instance records pass through byte-identical. Finding 2 — restrict automatic reuse to the active community. The reuse selectors filtered only on persona/command and channel membership, so provisioning a persona in community B attached and started the instance minted for A instead of minting B's own; the preset path matched on a deliberately generic runtime-id name with the same gap. Reuse is now gated on `managedAgentIsReusableInCommunity`, threaded as a required argument through every selector so no caller can omit it. That rule is deliberately stricter than the picker's visibility predicate: directory presence proves an identity runs here, which is the right signal for showing a row and the wrong one for adopting a record. Unscoped instances stay shared; an unresolved community fails closed, because minting a duplicate is recoverable and adopting another community's identity is the defect. An agent already in the channel is an established binding rather than a fresh adoption, so it stays unscoped. Finding 4 — use community scope for Welcome agent selection. Both selectors compared the legacy `relayUrl` creation pin, which a move deliberately leaves untouched, so a moved Welcome agent was invisible in the community it now belongs to — its replacement create then hit the new scoped name collision rule — and remained selectable in the one it left. Both now route through `managedAgentBelongsToCommunity`. The existing relay-scoped tests expressed the right intent through the wrong field and were moved onto `communityRelayUrl`. Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): enforce scoped name uniqueness on snapshot imports Addresses review finding 3. Both import paths stamped the active community scope onto minted instances but checked only for duplicate pubkeys under the store lock, so importing a snapshot named Bumble into a community that already offers Bumble persisted a second same-name scoped instance — recreating exactly the ambiguous picker entry this scoping exists to remove. Persona snapshot import now resolves its scope through `mint_scope_and_check_name`, the same helper `create_managed_agent` uses, so the check and the write share one critical section. Team snapshot import checks every member inside the lock, accumulating the members as it goes so they are validated against one another as well as against the existing store: one snapshot can carry two members that collide with each other while neither collides with anything already stored. Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): carry community scope through batch provisioning Addresses round-2 review findings 1, 2 and 3. 1. Thread the active community through batch provisioning. `createChannelManagedAgents` built its reuse context without `activeCommunityRelayUrl`, so every batch caller — add-team/add-bot dialogs, quick bot drop, mention-send, template application — handed the new reuse predicate `undefined`. A record bound to the CURRENT community then failed closed, was skipped, and provisioning fell through to `createManagedAgent`, where the new scoped name-uniqueness gate rejected the replacement: ordinary same-community reuse broken in the name of blocking cross-community adoption. The scope is now a required argument rather than an optional context field. `ChannelAgentCommunityContext` (`{ activeCommunityRelayUrl: string | null }`) is required by `createChannelManagedAgents` and `ensureChannelAgentPresetInChannel`; `ChannelAgentProvisionContext` extends it with the batch-shared `managedAgents` / `channelMemberPubkeys` and is required by `provisionChannelManagedAgent` and `createChannelManagedAgent`. A new call site cannot omit the scope without a type error, and `null` stays the explicit "unresolved community" value the reuse predicate fails closed on. Making the reuse inputs non-optional also retires the two `context?.managedAgents && context.channelMemberPubkeys` guards, which no caller could trip. Production callers updated: `useCreateChannelManagedAgentMutation`, `useCreateChannelManagedAgentsMutation`, and `useApplyTemplate` now read `useActiveCommunityRelayUrl()` and pass it down. 2. Restore the snapshot importer below the repository size ratchet. The uniqueness fix pushed `snapshot/import.rs` to 1,010 lines against a 1,000 ceiling, failing `check:file-sizes`. Its two inline `#[cfg(test)]` modules move verbatim to a sibling `snapshot/import_tests.rs`, matching the module's existing `snapshot/tests_*.rs` convention; only the two `use` paths changed (`super::` -> `super::super::`). import.rs is now 874 lines. 3. Run rustfmt on the team import change. `use crate::managed_agents::{...}` in `commands/team_snapshot.rs` had `AgentDefinition` split onto its own line; the three type names fit one line. Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): keep the boundary-7 egress test in the inventoried file Addresses round-3 review findings 1 and 2. 1. Preserve the security inventories when moving snapshot tests. Splitting BOTH inline test modules out of `snapshot/import.rs` was not the pure move I claimed: `egress_guard_tests.rs` scans source files and pins two inventories by path. `EVENTS_INVENTORY` attributes 2 egress-URL sites to import.rs (boundary 7 plus its in-file injection fixture), and the key-backup source allowlist lists import.rs by name. Relocating the fixture dropped import.rs to 1 site and put the fixture in a file on neither list, so `events_url_inventory_is_fully_guarded` and `ncryptsec_handling_is_confined_to_allowlisted_files` both failed. `egress_guard_tests` moves back inline, verbatim. The avatar tests alone carry the line-count relief (import.rs is 907 lines against the 1,000 ceiling), so nothing is gained by relocating the fixture — and the alternative, widening two security inventories to buy line count, is the wrong trade. `egress_guard_tests.rs` already documents this module as boundary 7's injection test by path, and that statement is true again. The sibling file's own header records why only the avatar tests live there, and it names no scanner needle of its own. 2. Apply rustfmt's actual wrapping. `save_teams` moves to the following line in the `crate::managed_agents` use group, per the reviewer's `cargo fmt --check` output. My previous rewrap was computed by hand rather than observed; the width arithmetic put that line at exactly 100 columns, and rustfmt does not accept it there. Signed-off-by: Junchao Yan <yjc801@gmail.com> * fix(desktop): extract channel-agent mutations to clear the size ratchet Rebasing onto main put `hooks.ts` at 994 lines before this branch touches it, so the four `useActiveCommunityRelayUrl()` reads that thread community scope through the batch provisioning hooks pushed it to 1006 — over the 1000-line ceiling `check:file-sizes` enforces. Moves the five channel-scoped provisioning mutations (attach, ensure preset, create one, provision, create batch) to a sibling `channelAgentMutations.ts`. That is exactly the cluster this branch grew, and it is the only user of `invalidateAgentQueriesInBackground` and `isCachedDmChannel`, which are now exported from `hooks.ts` rather than moved: the new module imports one-directionally, matching how `teamHooks.ts` sits under `hooks.ts` with no cycle. Consumers import the five hooks from the new path directly instead of re-exporting through `hooks.ts`, which would have made the dependency circular. `hooks.ts` is 781 lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Junchao Yan <yjc801@gmail.com> --------- Signed-off-by: Junchao Yan <yjc801@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Every agent, every community, the moment your GUI starts
On GUI startup, Desktop spawns an ACP harness per (agent, community) pair — every managed agent, in every community it belongs to. Warm authenticated sockets with narrow mention-scoped subscriptions; lazy LLM pool (the agent's brain wakes on the first message addressed to it, everything arriving during wake is held). Idle cost ~5 MB dirty per harness, zero tokens.
Why: mobile can publish a mention into any community's relay but can't spawn a harness on your desktop. Navigation-gated availability made reachability an accident of where the GUI was looking. This makes it a property of the agent — and kills the cold-mention race by construction (the summoning message is always already captured by a warm socket).
Not a new identity model: one keypair, one persona, one 30177 config per agent. Harnesses start only where the agent is already a member.
Structure (four lanes, merged in order)
ddcbb4321, Mari): process map, PID receipts, logs, session cache, per-pair workdirs re-keyed from pubkey toManagedAgentRuntimeKey; fail-closed replace-before-spawn seam for restart (receipt removed only after confirmed exit); bounded-concurrency startup; per-pair stop/restart.eccf18cd7, Wren): opt-in--lazy-pool/BUZZ_ACP_LAZY_POOL(eager remains default); relay auth/subscriptions/presence before any ACP child; exactly-one async wake on accepted flushable work; queued dispatch after Ready; bounded 5s→5m retry; stale/duplicate wake rejection;managed_agent_runtime_lifecycleobserver frames (listening/waking/ready/failed); explicit shutdown cleanup.e29aa1673, Max): pair-scoped runtime state, lifecycle presence in UI, settings per-pair controls.e0ca4dd33, Perci): integration matrix incl. native replacement probe (old PID dead, exactly one live process + one receipt).Verification
Each lane independently reviewed (line review + delta-confirms + mutation-tested unit tests + live native probes; ledger in the build thread). At the integrated head
0f313d2ed(post merge of main31090b186):cargo fmt --all --checkclean, fullbuzz-acpsuite 541 unit + 9 lifecycle, desktop tauri lib 1484 pass,tsc --noEmitclean, desktop unit 3185 pass, all pre-commit/pre-push hooks green.Known follow-ups (flagged, not in this PR)
to_startfilter (correct while restore is record-level, one relay per record); the multi-relay follow-up must key-scope that filter or add the map check to the replacement helper.Redteam + review fix round (head
060c100a8)Seven-lane redteam at
0f313d2ed(2 PASS: crypto/lifecycle-spoof defense verified with live probes; real-native-process replacement/poison battery held) plus Wes review produced four fix lanes, all merged and re-gated:lazy=true(no idle brains on ordinary launch); Phase B takes a live-map guard mirroringstart_pair, so a concurrent reconcile's live child is skipped, never terminated (SpawnOutcome::{Spawned,Skipped,Failed}).BUZZ_MANAGED_AGENT_START_NONCE) carried in every signed lifecycle frame and compared before mutation — replayed genuine frames from a prior harness generation are rejected even while a replacement child is alive (also zeroes the malicious-relay replay ceiling);list_managed_agent_runtimesreaps exited pairs under the transition/store locks and emits explicit Stopped (no stale "Here" on a dead PID); reconcile carries the recordupdated_atfreshness boundary so an intervening user Stop/edit wins; reconcile filters onstart_on_app_launch— "every agent everywhere" means every auto-start agent everywhere; an explicit opt-out survives. Plus review hygiene: 64-hex pubkey validation/canonicalization inManagedAgentRuntimeKey,eq_ignore_ascii_case, deadManagedAgentRuntimeTargetand legacywrite_agent_pid_fileremoved.ChannelScreenwires the active community relay intoMembersSidebar— pair truth when known, legacy Running/Stopped when unknown, no false "Unavailable".BUZZ_E2E_AGENTS_EVERYWHERE=1, own config) drives two isolated local relays with the same agent identity: zero ACP subprocesses before mention, idle RSS/process counts recorded, exactly one correctly-isolated ACK per relay, no owned PIDs after teardown. Excluded from ordinary smoke runs.Gates at the integrated head (same shell as
git rev-parse HEAD=060c100a8, post merge of main768cd4dbe):cargo fmt --all --checkclean; clippy-D warningsfor desktop tauri (all targets) and buzz-acp; buzz-acp suite 542; desktop tauri 1486 + 3;tsc --noEmit; desktop unit 3190/3190; production build; smoke acceptancechannels.spec.ts:2898(members sidebar respawn) passed;pnpm checkfile-size/px/pubkey gates clean; pre-push full hook suite green.Round-2 fix: agent-wide lifecycle fan-out (head after
060c100a8)Max's independent review at
060c100a8found the legacy agent-wide paths never fanned out to pair runtimes. Fixed in a three-commit lane (Wren, verified independently by Max and Eva, accepted at exact tip):start_pairpath (fresh generation nonces, receipts, live-map guard) — never the scalar/eager legacy start. Candidate pre-filters now inspect live pair runtimes instead of the obsoleterecord.runtime_pid(always None under pair keying — the old pre-check would have silently disabled config-change restarts). The factored pair preflight retains the persona re-snapshot for 30177 relay sync, matching the legacy preflight contract.Additional named fast-follows from this round:
Documented limitations (ruled, not fixed here)